
class Connection {


// PARAMETERS
  // number of frames it takes for the spread to complete
  final int spreadingFrameCount = 25; 

  // number of frames to connect between two individuals
  final int connectingFrameCount = 25;

  // note: Connection dies on size = 0
  final float maxSize = 5;
  final float sizeIncreaseOnSpread = 1.5;
  final float sizeDecreaseOnDeny = 0.2;
  // decrease one in ten seconds
  final float sizeDecayRate = 1 / (10 * frameRate);

  // colors for channel drawing
  final color chColDark = color(63);
  final float chColDarkA = 40;

  final color chColLight = color(203);
  final color chColLightA = 150;

  // colors for inter group channels
  final color chColInterGroup = color(203, 203, 233);
  final float chColInterGroupALow = 15;
  final float chColInterGroupAHigh = 150;

  // thickness for connections and for the border
  final float chWeightThick = 25;
  final float chWeightThin = 15;
  final float chBorderThickness = 4;

  final float hilightOpacityIncrease = 20;

  final float connectionWidth = 8;
  final float spreadDotSize = 8;

  final color hilightColor = color(230);
  final color lineColor = color(5, 128, 255);
  final color backgroundColor = color(63);
  
  final float pushStrenght = 10; // strenght of force to push appart the entites on successful infection

  // Tail fade factor change
  final float fadeFactorChange = 0.1;

  // Tail opacity change
  float tailOpacityChange = 1.0; 
  
  // states
  final int SPREADING = 1;
  final int CONNECTING = 2;
  final int IDLE = 3;
// PARAMETERS END

// MEMBERS
  // connection between two entities
  Individual from, to;

  boolean isCloudPull;
  boolean isInterGroup;

  // the meme that spreads throught the connection
  Meme meme;

  // step counter for animating states
  float currentStep = 0.0;

  // current opacity
  float opacity = 0.0;

  // current state
  int state = 0;
  
  // Tail opacity
  float tailOpacity = 80.0;
 
  // Tail Fade factor
  float fadeFactor = 0.0;
  
  // midpoint between entities when connecting and spreaing
  float midx, midy;

  // angle between entities
  float angle;

  // distance between the two individuals
  float len;

  float completionRatio;
  
  // TODO: replace with another?
  float size = 10.0;

  // bonus for spreding through this connection
  float spreadBonus;

  boolean macroMode;

  SocialGroup parent;
// END MEMBERS

  public Connection(
      SocialGroup parent, 
      Individual from, Individual to, 
      Meme meme) {

    init(parent, from, to, meme);
    isCloudPull = false;
  }

  public Connection(
      SocialGroup parent,
      Meme cloudMeme, 
      Individual to) {
    
    // create a temporary individual
    // that deletes when the spread is complete
    Individual memeHolder = new Individual(parent);
    memeHolder.p.moveTo(
        cloudMeme.p.position().x(),
        cloudMeme.p.position().y(), 0);

    cloudMeme.drawOpacity = 255;

    init(parent, memeHolder, to, cloudMeme);

    // always get infected by pulls
    spreadBonus = 1.0;
    isCloudPull = true;
  }

  private void init(
      SocialGroup parent, 
      Individual from, Individual to, 
      Meme meme) {

    this.parent = parent;
    this.from = from;
    this.to = to;
    this.meme = meme;
    this.spreadBonus = 0.0;
    this.isInterGroup = from.parent != to.parent;
    this.size = 1.0;
      
    if(from == null || to == null || meme == null || parent == null)
      println("Warning: Connection created with NULL argument");

    from.createdConnections++;

    from.connections.add(this);
    to.connections.add(this);

    to.incomingMeme = meme;

    state = CONNECTING;
  }

  private void suicide() {
    // println("Killing myslef");

    to.connections.remove(this);
    from.connections.remove(this);
    parent.connections.remove(this);
    tailOpacity = 100.0;
  }

  private void updateMidPoint() {
    float tweenx, tweeny;

    if(state == CONNECTING) 
      completionRatio = (float)currentStep / connectingFrameCount;
    else if (state == SPREADING)
      completionRatio = (float)currentStep / spreadingFrameCount;
    else return;

    if(isCloudPull) 
      from.p.moveTo(meme.absx(), meme.absy(), 0);

    tweenx = from.x() - to.x();
    tweeny = from.y() - to.y();
    angle = atan2(tweenx, tweeny);
    len = sqrt(tweenx*tweenx+tweeny*tweeny);
  
    midx = from.x() - sin(angle) * len * completionRatio;
    midy = from.y() - cos(angle) * len * completionRatio;
  }

  // width RATIO and alpha boost (hilight)
  private void drawChannelBackground(float ratio, float hilight) {

    // fade out connections as the decompose
    float fade = size <= 1 ? size : 1;
    ratio *= fade;

    strokeWeight((chWeightThick + size) * ratio + size);

    // draw the border for micro mode
    if(!isInterGroup) {
      stroke(chColDark, fade * (chColDarkA + hilight));
      thickLine(from.x(), from.y(), to.x(), to.y());
      strokeWeight(((chWeightThick + 2) - 2 * chBorderThickness) * ratio + size);
    }

    if(isInterGroup) {
      if(macroMode) {
        stroke(chColInterGroup, fade * (chColInterGroupAHigh + hilight));
        strokeWeight(chWeightThin * ratio);
      }
      else 
        stroke(chColInterGroup, fade * (chColInterGroupALow + hilight));
    }
    else {
      if(size <= 1)
        stroke(
          mergeColor(
            parent.fillColorMembrane, 
            chColLight, 
            fade * (hilight + chColLightA) / 255));

      else // draw with a opaque, faded color
        stroke(
          mergeColor(
            parent.fillColorMembrane, 
            chColLight, 
            (hilight + chColLightA) / 255));
    }
    
    thickLine(from.x(), from.y(), to.x(), to.y());    
  }

  private void drawPulling() {
    
    stroke(210, 210, 210);
    strokeWeight(4);
    line(to.x() + sin(angle) * len * completionRatio, 
        to.y() + cos(angle) * len * completionRatio, 
        to.x(), to.y());

    // hilight the meme thats about to be pulled
    stroke(meme.hilightColor);
    noFill();
    strokeWeight(2);
    ellipse(meme.x(), meme.y(), zoom * meme.size, zoom * meme.size);

    strokeWeight(1);
  }

  private void drawConnecting() {
    if(isCloudPull) {
      drawPulling();
      return;
    }
    else
      drawChannelBackground(completionRatio, 0);
  }

  private void drawSpreading() {
    if(!isCloudPull) {
      drawChannelBackground(1, hilightOpacityIncrease);
    }

    if(macroMode){
      if(isCloudPull) {
        meme.drawAt(midx, midy);
        stroke(chColInterGroup, chColInterGroupAHigh);
        strokeWeight(chWeightThin);
        line(to.x(), to.y(), midx, midy);
      }
      // draw normal spread in macro mode
      else {
        noStroke();
        fill(meme.c);
        ellipse(midx, midy, spreadDotSize*2, spreadDotSize*2);
      }
    }
    else{
      strokeWeight(8*zoom);
      stroke(meme.c);

      if(!isCloudPull)
        line(from.x(), from.y(), midx, midy);
      else
        line(midx, midy, to.x(), to.y());

      fill(0,0,0);
      ellipse(midx, midy, 4*zoom, 4*zoom);
      strokeWeight(1);
      fill(63);

      /*
      textFont(smallFont, 12);
      text("type=" + meme.type, midx + 10, midy);
      text("sprc=" + meme.getNumSpreaders(), midx + 10, midy + 20);
      */

    }

  }


  private void drawIdle() {
    drawChannelBackground(1.0, 0);
  }

  public void drawInterConnection() {
    if(from.parent != to.parent)
      draw();
  }

  public void drawNonInterConnection() {
    if(from.parent == to.parent)
      draw();
  }

  public void draw() {
    updateMidPoint();

    macroMode = (zoom < macroZoom);

    // draw macro modes
    if(state == CONNECTING) drawConnecting();
    if(state == SPREADING) drawSpreading();
    if(state == IDLE) drawIdle();
  }

  // returns true if the connection is between the entity a and b
  public boolean isBetween(Entity a, Entity b) {
    return (from == a && to == b) || (from == b && to == a);
  }

  public boolean includes(Individual a) {
    return (from == a) || (to == a);
  }

  // flips the from and to sides
  public void flip() {
    Individual tmp = from;
    from = to;
    to = tmp;
  }

  public Individual other(Individual ind) {
    if(from == ind) return to;
    if(to == ind) return from;
    return null;
  }

  public boolean viableForSpread(Individual from, Meme meme) {
    return state == IDLE && !other(from).hasMeme(meme);
  }

  // update the frames
  public void update() {
    if(state == IDLE) {
      size -= sizeDecayRate * timeScale;
      if(size <= 0) suicide();
    }

    currentStep += timeScale;

    if(state == SPREADING && (currentStep >= spreadingFrameCount)) {

      // call back for completed spread
      if(to.infect(from, meme, spreadBonus)) {
        size += sizeIncreaseOnSpread;
        size = size > maxSize ? maxSize : size;
      }
      else {
        size -= sizeDecreaseOnDeny;
      }

      // destroy the pulling connections
      if(isCloudPull) {
        meme.drawOpacity = meme.defaultMemeOpacity;
        suicide();
      }

      state = IDLE;

      return;
    }

    else if((state == CONNECTING) && 
        (currentStep >= connectingFrameCount)) {
      // connect complete, spread the meme
      startSpread(meme, spreadBonus);
      
      // remove it from the cloud
      if(isCloudPull) theCloud.memes.remove(meme);
      return;
    }
  }

  public void startSpread(Meme meme, float bonus) {
    this.spreadBonus = bonus;
    this.meme = meme;
    to.incomingMeme = meme;
    currentStep = 0;
    state = SPREADING;
  }


}
